// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Threading;
using System.Threading.Tasks;

namespace System.Runtime.CompilerServices
{
    /// <summary>
    /// Provides a builder for asynchronous methods that return <see cref="Task{TResult}"/>.
    /// This type is intended for compiler use only.
    /// </summary>
    /// <remarks>
    /// AsyncTaskMethodBuilder{TResult} is a value type, and thus it is copied by value.
    /// Prior to being copied, one of its Task, SetResult, or SetException members must be accessed,
    /// or else the copies may end up building distinct Task instances.
    /// </remarks>
    public struct AsyncTaskMethodBuilder<TResult>
    {
        /// <summary>The lazily-initialized built task.</summary>
        private Task<TResult>? m_task; // Debugger depends on the exact name of this field.

        /// <summary>Initializes a new <see cref="AsyncTaskMethodBuilder"/>.</summary>
        /// <returns>The initialized <see cref="AsyncTaskMethodBuilder"/>.</returns>
        public static AsyncTaskMethodBuilder<TResult> Create() => default;

        /// <summary>Initiates the builder's execution with the associated state machine.</summary>
        /// <typeparam name="TStateMachine">Specifies the type of the state machine.</typeparam>
        /// <param name="stateMachine">The state machine instance, passed by reference.</param>
        [DebuggerStepThrough]
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public void Start<TStateMachine>(ref TStateMachine stateMachine) where TStateMachine : IAsyncStateMachine =>
            AsyncMethodBuilderCore.Start(ref stateMachine);

        /// <summary>Associates the builder with the state machine it represents.</summary>
        /// <param name="stateMachine">The heap-allocated state machine object.</param>
        /// <exception cref="ArgumentNullException">The <paramref name="stateMachine"/> argument was null (<see langword="Nothing" /> in Visual Basic).</exception>
        /// <exception cref="InvalidOperationException">The builder is incorrectly initialized.</exception>
        public void SetStateMachine(IAsyncStateMachine stateMachine) =>
            AsyncMethodBuilderCore.SetStateMachine(stateMachine, m_task);

        /// <summary>
        /// Schedules the specified state machine to be pushed forward when the specified awaiter completes.
        /// </summary>
        /// <typeparam name="TAwaiter">Specifies the type of the awaiter.</typeparam>
        /// <typeparam name="TStateMachine">Specifies the type of the state machine.</typeparam>
        /// <param name="awaiter">The awaiter.</param>
        /// <param name="stateMachine">The state machine.</param>
        public void AwaitOnCompleted<TAwaiter, TStateMachine>(
            ref TAwaiter awaiter, ref TStateMachine stateMachine)
            where TAwaiter : INotifyCompletion
            where TStateMachine : IAsyncStateMachine =>
            AwaitOnCompleted(ref awaiter, ref stateMachine, ref m_task);

        internal static void AwaitOnCompleted<TAwaiter, TStateMachine>(
            ref TAwaiter awaiter, ref TStateMachine stateMachine, ref Task<TResult>? taskField)
            where TAwaiter : INotifyCompletion
            where TStateMachine : IAsyncStateMachine
        {
            try
            {
                awaiter.OnCompleted(GetStateMachineBox(ref stateMachine, ref taskField).MoveNextAction);
            }
            catch (Exception e)
            {
                Threading.Tasks.Task.ThrowAsync(e, targetContext: null);
            }
        }

        /// <summary>
        /// Schedules the specified state machine to be pushed forward when the specified awaiter completes.
        /// </summary>
        /// <typeparam name="TAwaiter">Specifies the type of the awaiter.</typeparam>
        /// <typeparam name="TStateMachine">Specifies the type of the state machine.</typeparam>
        /// <param name="awaiter">The awaiter.</param>
        /// <param name="stateMachine">The state machine.</param>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public void AwaitUnsafeOnCompleted<TAwaiter, TStateMachine>(
            ref TAwaiter awaiter, ref TStateMachine stateMachine)
            where TAwaiter : ICriticalNotifyCompletion
            where TStateMachine : IAsyncStateMachine =>
            AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine, ref m_task);

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        internal static void AwaitUnsafeOnCompleted<TAwaiter, TStateMachine>(
            ref TAwaiter awaiter, ref TStateMachine stateMachine, [NotNull] ref Task<TResult>? taskField)
            where TAwaiter : ICriticalNotifyCompletion
            where TStateMachine : IAsyncStateMachine
        {
            IAsyncStateMachineBox box = GetStateMachineBox(ref stateMachine, ref taskField);
            AwaitUnsafeOnCompleted(ref awaiter, box);
        }

        // Tier0 codegen for this function may still allocate (while FullOpts won't).
        // TODO: remove once https://github.com/dotnet/runtime/issues/90965 is implemented
        [MethodImpl(MethodImplOptions.AggressiveOptimization)]
        internal static void AwaitUnsafeOnCompleted<TAwaiter>(
            ref TAwaiter awaiter, IAsyncStateMachineBox box)
            where TAwaiter : ICriticalNotifyCompletion
        {
            // The null tests here ensure that the jit can optimize away the interface
            // tests when TAwaiter is a ref type.

            if ((null != (object?)default(TAwaiter)) && (awaiter is ITaskAwaiter))
            {
                ref TaskAwaiter ta = ref Unsafe.As<TAwaiter, TaskAwaiter>(ref awaiter); // relies on TaskAwaiter/TaskAwaiter<T> having the same layout
                TaskAwaiter.UnsafeOnCompletedInternal(ta.m_task, box, continueOnCapturedContext: true);
            }
            else if ((null != (object?)default(TAwaiter)) && (awaiter is IConfiguredTaskAwaiter))
            {
                ref ConfiguredTaskAwaitable.ConfiguredTaskAwaiter ta = ref Unsafe.As<TAwaiter, ConfiguredTaskAwaitable.ConfiguredTaskAwaiter>(ref awaiter);
                TaskAwaiter.UnsafeOnCompletedInternal(ta.m_task, box, (ta.m_options & ConfigureAwaitOptions.ContinueOnCapturedContext) != 0);
            }
            else if ((null != (object?)default(TAwaiter)) && (awaiter is IStateMachineBoxAwareAwaiter))
            {
                try
                {
                    ((IStateMachineBoxAwareAwaiter)awaiter).AwaitUnsafeOnCompleted(box);
                }
                catch (Exception e)
                {
                    // Whereas with Task the code that hooks up and invokes the continuation is all local to corelib,
                    // with ValueTaskAwaiter we may be calling out to an arbitrary implementation of IValueTaskSource
                    // wrapped in the ValueTask, and as such we protect against errant exceptions that may emerge.
                    // We don't want such exceptions propagating back into the async method, which can't handle
                    // exceptions well at that location in the state machine, especially if the exception may occur
                    // after the ValueTaskAwaiter already successfully hooked up the callback, in which case it's possible
                    // two different flows of execution could end up happening in the same async method call.
                    Threading.Tasks.Task.ThrowAsync(e, targetContext: null);
                }
            }
            else
            {
                // The awaiter isn't specially known. Fall back to doing a normal await.
                try
                {
                    awaiter.UnsafeOnCompleted(box.MoveNextAction);
                }
                catch (Exception e)
                {
                    Threading.Tasks.Task.ThrowAsync(e, targetContext: null);
                }
            }
        }

        /// <summary>Gets the "boxed" state machine object.</summary>
        /// <typeparam name="TStateMachine">Specifies the type of the async state machine.</typeparam>
        /// <param name="stateMachine">The state machine.</param>
        /// <param name="taskField">The reference to the Task field storing the Task instance.</param>
        /// <returns>The "boxed" state machine.</returns>
        private static IAsyncStateMachineBox GetStateMachineBox<TStateMachine>(
            ref TStateMachine stateMachine,
            [NotNull] ref Task<TResult>? taskField)
            where TStateMachine : IAsyncStateMachine
        {
            ExecutionContext? currentContext = ExecutionContext.Capture();

            IAsyncStateMachineBox result;

            // Check first for the most common case: not the first yield in an async method.
            // In this case, the first yield will have already "boxed" the state machine in
            // a strongly-typed manner into an AsyncStateMachineBox.  It will already contain
            // the state machine as well as a MoveNextDelegate and a context.  The only thing
            // we might need to do is update the context if that's changed since it was stored.
            if (taskField is AsyncStateMachineBox<TStateMachine> stronglyTypedBox)
            {
                if (stronglyTypedBox.Context != currentContext)
                {
                    stronglyTypedBox.Context = currentContext;
                }
                result = stronglyTypedBox;
            }
            // The least common case: we have a weakly-typed boxed.  This results if the debugger
            // or some other use of reflection accesses a property like ObjectIdForDebugger or a
            // method like SetNotificationForWaitCompletion prior to the first await happening.  In
            // such situations, we need to get an object to represent the builder, but we don't yet
            // know the type of the state machine, and thus can't use TStateMachine.  Instead, we
            // use the IAsyncStateMachine interface, which all TStateMachines implement.  This will
            // result in a boxing allocation when storing the TStateMachine if it's a struct, but
            // this only happens in active debugging scenarios where such performance impact doesn't
            // matter.
            else if (taskField is AsyncStateMachineBox<IAsyncStateMachine> weaklyTypedBox)
            {
                // If this is the first await, we won't yet have a state machine, so store it.
                if (weaklyTypedBox.StateMachine == null)
                {
                    Debugger.NotifyOfCrossThreadDependency(); // same explanation as with usage below
                    weaklyTypedBox.StateMachine = stateMachine;
                }

                // Update the context.  This only happens with a debugger, so no need to spend
                // extra IL checking for equality before doing the assignment.
                weaklyTypedBox.Context = currentContext;
                result = weaklyTypedBox;
            }
            else
            {
                // Alert a listening debugger that we can't make forward progress unless it slips threads.
                // If we don't do this, and a method that uses "await foo;" is invoked through funceval,
                // we could end up hooking up a callback to push forward the async method's state machine,
                // the debugger would then abort the funceval after it takes too long, and then continuing
                // execution could result in another callback being hooked up.  At that point we have
                // multiple callbacks registered to push the state machine, which could result in bad behavior.
                Debugger.NotifyOfCrossThreadDependency();

                // At this point, taskField should really be null, in which case we want to create the box.
                // However, in a variety of debugger-related (erroneous) situations, it might be non-null,
                // e.g. if the Task property is examined in a Watch window, forcing it to be lazily-initialized
                // as a Task<TResult> rather than as an AsyncStateMachineBox.  The worst that happens in such
                // cases is we lose the ability to properly step in the debugger, as the debugger uses that
                // object's identity to track this specific builder/state machine.  As such, we proceed to
                // overwrite whatever's there anyway, even if it's non-null.
#if NATIVEAOT
                // DebugFinalizableAsyncStateMachineBox looks like a small type, but it actually is not because
                // it will have a copy of all the slots from its parent. It will add another hundred(s) bytes
                // per each async method in NativeAOT binaries without adding much value. Avoid
                // generating this extra code until a better solution is implemented.
                var box = new AsyncStateMachineBox<TStateMachine>();
#else
                AsyncStateMachineBox<TStateMachine> box = AsyncMethodBuilderCore.TrackAsyncMethodCompletion ?
                    CreateDebugFinalizableAsyncStateMachineBox<TStateMachine>() :
                    new AsyncStateMachineBox<TStateMachine>();
#endif
                taskField = box; // important: this must be done before storing stateMachine into box.StateMachine!
                box.StateMachine = stateMachine;
                box.Context = currentContext;

                // Log the creation of the state machine box object / task for this async method.
                if (TplEventSource.Log.IsEnabled())
                {
                    AsyncMethodBuilderCore.LogTraceOperationBegin(box, stateMachine.GetType());
                }

                // And if async debugging is enabled, track the task.
                if (Threading.Tasks.Task.s_asyncDebuggingEnabled)
                {
                    Threading.Tasks.Task.AddToActiveTasks(box);
                }
                result = box;
            }

            return result;
        }

#if !NATIVEAOT
        // Avoid forcing the JIT to build DebugFinalizableAsyncStateMachineBox<TStateMachine> unless it's actually needed.
        [MethodImpl(MethodImplOptions.NoInlining)]
        private static AsyncStateMachineBox<TStateMachine> CreateDebugFinalizableAsyncStateMachineBox<TStateMachine>()
            where TStateMachine : IAsyncStateMachine =>
            new DebugFinalizableAsyncStateMachineBox<TStateMachine>();

        /// <summary>
        /// Provides an async state machine box with a finalizer that will fire an EventSource
        /// event about the state machine if it's being finalized without having been completed.
        /// </summary>
        /// <typeparam name="TStateMachine">Specifies the type of the state machine.</typeparam>
        private sealed class DebugFinalizableAsyncStateMachineBox<TStateMachine> : // SOS DumpAsync command depends on this name
            AsyncStateMachineBox<TStateMachine>
            where TStateMachine : IAsyncStateMachine
        {
            ~DebugFinalizableAsyncStateMachineBox()
            {
                // If the state machine is being finalized, something went wrong during its processing,
                // e.g. it awaited something that got collected without itself having been completed.
                // Fire an event with details about the state machine to help with debugging.
                if (!IsCompleted) // double-check it's not completed, just to help minimize false positives
                {
                    TplEventSource.Log.IncompleteAsyncMethod(this);
                }
            }
        }
#endif

        /// <summary>A strongly-typed box for Task-based async state machines.</summary>
        /// <typeparam name="TStateMachine">Specifies the type of the state machine.</typeparam>
        [DebuggerDisplay("{DebuggerDisplay,nq}")]
        private class AsyncStateMachineBox<TStateMachine> : // SOS DumpAsync command depends on this name
            Task<TResult>, IAsyncStateMachineBox
            where TStateMachine : IAsyncStateMachine
        {
            /// <summary>Delegate used to invoke on an ExecutionContext when passed an instance of this box type.</summary>
            private static readonly ContextCallback s_callback = ExecutionContextCallback;

            // Used to initialize s_callback above. We don't use a lambda for this on purpose: a lambda would
            // introduce a new generic type behind the scenes that comes with a hefty size penalty in AOT builds.
            private static void ExecutionContextCallback(object? s)
            {
                Debug.Assert(s is AsyncStateMachineBox<TStateMachine>);
                // Only used privately to pass directly to EC.Run
                Unsafe.As<AsyncStateMachineBox<TStateMachine>>(s).StateMachine!.MoveNext();
            }

            /// <summary>The state machine itself.</summary>
            public TStateMachine? StateMachine; // mutable struct; do not make this readonly. SOS DumpAsync command depends on this name.

            public AsyncStateMachineBox()
            {
                // The async state machine uses the base Task's state object field to store the captured execution context.
                // Ensure that state object isn't published out for others to see.
                Debug.Assert((m_stateFlags & (int)InternalTaskOptions.PromiseTask) != 0, "Expected state flags to already be configured.");
                Debug.Assert(m_stateObject is null, "Expected to be able to use the state object field for ExecutionContext.");
                m_stateFlags |= (int)InternalTaskOptions.HiddenState;
            }

            /// <summary>Debugger-only display string for the async state machine.</summary>
            private string DebuggerDisplay
            {
                get
                {
                    // Ideally we just use the type of the TStateMachine as the "method" name.  However, in certain use in the
                    // debugger, TStateMachine might actually be a weakly-typed IAsyncStateMachine, in which case we can ToString
                    // the state machine instance.  But in debug builds the state machine type could also be a class, in which case
                    // the field could be null, so worst case we just fall back to using "IAsyncStateMachine".
                    string stateMachineName = typeof(TStateMachine) != typeof(IAsyncStateMachine) ?
                        typeof(TStateMachine).Name :
                        StateMachine?.ToString() ??
                        nameof(IAsyncStateMachine);

                    // Keep the shape of this message in sync with that of the base Task<TResult>.
                    return IsCompletedSuccessfully && typeof(TResult) != typeof(VoidTaskResult) ?
                        $"Id = {Id}, Status = {Status}, Method = {stateMachineName}, Result = {m_result}" :
                        $"Id = {Id}, Status = {Status}, Method = {stateMachineName}";
                }
            }

            /// <summary>A delegate to the <see cref="MoveNext()"/> method.</summary>
            public Action MoveNextAction => (Action)(m_action ??= new Action(MoveNext));

            /// <summary>Captured ExecutionContext with which to invoke <see cref="MoveNextAction"/>; may be null.</summary>
            /// <remarks>
            /// This uses the base Task.m_stateObject field to store the context, as that field is otherwise unused for state machine boxes.
            /// This *must* not be set to anything other than null or an ExecutionContext, or it will result in a type safety hole.
            /// We also don't want this ExecutionContext exposed out to consumers of the Task via Task.AsyncState, so
            /// the ctor sets the HiddenState option to prevent this from leaking out.
            /// </remarks>
            public ref ExecutionContext? Context
            {
                get
                {
                    Debug.Assert(m_stateObject is null or ExecutionContext, $"Expected {nameof(m_stateObject)} to be null or an ExecutionContext but was {(m_stateObject is object o ? o.GetType().ToString() : "(null)")}.");
                    return ref Unsafe.As<object?, ExecutionContext?>(ref m_stateObject);
                }
            }

            internal sealed override void ExecuteFromThreadPool(Thread threadPoolThread) => MoveNext(threadPoolThread);

            /// <summary>Calls MoveNext on <see cref="StateMachine"/></summary>
            public void MoveNext() => MoveNext(threadPoolThread: null);

            private void MoveNext(Thread? threadPoolThread)
            {
                Debug.Assert(!IsCompleted);

                bool loggingOn = TplEventSource.Log.IsEnabled();
                if (loggingOn)
                {
                    TplEventSource.Log.TraceSynchronousWorkBegin(this.Id, CausalitySynchronousWork.Execution);
                }

                ExecutionContext? context = Context;
                if (context == null)
                {
                    Debug.Assert(StateMachine != null);
                    StateMachine.MoveNext();
                }
                else
                {
                    if (threadPoolThread is null)
                    {
                        ExecutionContext.RunInternal(context, s_callback, this);
                    }
                    else
                    {
                        ExecutionContext.RunFromThreadPoolDispatchLoop(threadPoolThread, context, s_callback, this);
                    }
                }

                if (IsCompleted)
                {
                    ClearStateUponCompletion();
                }

                if (loggingOn)
                {
                    TplEventSource.Log.TraceSynchronousWorkEnd(CausalitySynchronousWork.Execution);
                }
            }

            /// <summary>Clears out all state associated with a completed box.</summary>
            [MethodImpl(MethodImplOptions.AggressiveInlining)]
            public void ClearStateUponCompletion()
            {
                Debug.Assert(IsCompleted);

                // This logic may be invoked multiple times on the same instance and needs to be robust against that.

                // If async debugging is enabled, remove the task from tracking.
                if (s_asyncDebuggingEnabled)
                {
                    RemoveFromActiveTasks(this);
                }

                // Clear out state now that the async method has completed.
                // This avoids keeping arbitrary state referenced by lifted locals
                // if this Task / state machine box is held onto.
                StateMachine = default;
                Context = default;

#if !NATIVEAOT
                // In case this is a state machine box with a finalizer, suppress its finalization
                // as it's now complete.  We only need the finalizer to run if the box is collected
                // without having been completed.
                if (AsyncMethodBuilderCore.TrackAsyncMethodCompletion)
                {
                    GC.SuppressFinalize(this);
                }
#endif
            }

            /// <summary>Gets the state machine as a boxed object.  This should only be used for debugging purposes.</summary>
            IAsyncStateMachine IAsyncStateMachineBox.GetStateMachineObject() => StateMachine!; // likely boxes, only use for debugging
        }

        /// <summary>Gets the <see cref="Task{TResult}"/> for this builder.</summary>
        /// <returns>The <see cref="Task{TResult}"/> representing the builder's asynchronous operation.</returns>
        public Task<TResult> Task
        {
            [MethodImpl(MethodImplOptions.AggressiveInlining)]
            get => m_task ?? InitializeTaskAsPromise();
        }

        /// <summary>
        /// Initializes the task, which must not yet be initialized.  Used only when the Task is being forced into
        /// existence when no state machine is needed, e.g. when the builder is being synchronously completed with
        /// an exception, when the builder is being used out of the context of an async method, etc.
        /// </summary>
        [MethodImpl(MethodImplOptions.NoInlining)]
        private Task<TResult> InitializeTaskAsPromise()
        {
            Debug.Assert(m_task == null);
            return m_task = new Task<TResult>();
        }

        internal static Task<TResult> CreateWeaklyTypedStateMachineBox()
        {
#if NATIVEAOT
            // DebugFinalizableAsyncStateMachineBox looks like a small type, but it actually is not because
            // it will have a copy of all the slots from its parent. It will add another hundred(s) bytes
            // per each async method in NativeAOT binaries without adding much value. Avoid
            // generating this extra code until a better solution is implemented.
            return new AsyncStateMachineBox<IAsyncStateMachine>();
#else
            return AsyncMethodBuilderCore.TrackAsyncMethodCompletion ?
                CreateDebugFinalizableAsyncStateMachineBox<IAsyncStateMachine>() :
                new AsyncStateMachineBox<IAsyncStateMachine>();
#endif
        }

        /// <summary>
        /// Completes the <see cref="Task{TResult}"/> in the
        /// <see cref="TaskStatus">RanToCompletion</see> state with the specified result.
        /// </summary>
        /// <param name="result">The result to use to complete the task.</param>
        /// <exception cref="InvalidOperationException">The task has already completed.</exception>
        public void SetResult(TResult result)
        {
            // Get the currently stored task, which will be non-null if get_Task has already been accessed.
            // If there isn't one, get a task and store it.
            if (m_task is null)
            {
                m_task = Threading.Tasks.Task.FromResult(result);
            }
            else
            {
                // Slow path: complete the existing task.
                SetExistingTaskResult(m_task, result);
            }
        }

        /// <summary>Completes the already initialized task with the specified result.</summary>
        /// <param name="result">The result to use to complete the task.</param>
        /// <param name="task">The task to complete.</param>
        internal static void SetExistingTaskResult(Task<TResult> task, TResult? result)
        {
            Debug.Assert(task != null, "Expected non-null task");

            if (TplEventSource.Log.IsEnabled())
            {
                TplEventSource.Log.TraceOperationEnd(task.Id, AsyncCausalityStatus.Completed);
            }

            if (!task.TrySetResult(result))
            {
                ThrowHelper.ThrowInvalidOperationException(ExceptionResource.TaskT_TransitionToFinal_AlreadyCompleted);
            }
        }

        /// <summary>
        /// Completes the <see cref="Task{TResult}"/> in the
        /// <see cref="TaskStatus">Faulted</see> state with the specified exception.
        /// </summary>
        /// <param name="exception">The <see cref="Exception"/> to use to fault the task.</param>
        /// <exception cref="ArgumentNullException">The <paramref name="exception"/> argument is null (<see langword="Nothing" /> in Visual Basic).</exception>
        /// <exception cref="InvalidOperationException">The task has already completed.</exception>
        public void SetException(Exception exception) => SetException(exception, ref m_task);

        internal static void SetException(Exception exception, ref Task<TResult>? taskField)
        {
            if (exception == null)
            {
                ThrowHelper.ThrowArgumentNullException(ExceptionArgument.exception);
            }

            // Get the task, forcing initialization if it hasn't already been initialized.
            Task<TResult> task = (taskField ??= new Task<TResult>());

            // If the exception represents cancellation, cancel the task.  Otherwise, fault the task.
            bool successfullySet = exception is OperationCanceledException oce ?
                task.TrySetCanceled(oce.CancellationToken, oce) :
                task.TrySetException(exception);

            // Unlike with TaskCompletionSource, we do not need to spin here until _taskAndStateMachine is completed,
            // since AsyncTaskMethodBuilder.SetException should not be immediately followed by any code
            // that depends on the task having completely completed.  Moreover, with correct usage,
            // SetResult or SetException should only be called once, so the Try* methods should always
            // return true, so no spinning would be necessary anyway (the spinning in TCS is only relevant
            // if another thread completes the task first).
            if (!successfullySet)
            {
                ThrowHelper.ThrowInvalidOperationException(ExceptionResource.TaskT_TransitionToFinal_AlreadyCompleted);
            }
        }

        /// <summary>
        /// Called by the debugger to request notification when the first wait operation
        /// (await, Wait, Result, etc.) on this builder's task completes.
        /// </summary>
        /// <param name="enabled">
        /// true to enable notification; false to disable a previously set notification.
        /// </param>
        /// <remarks>
        /// This should only be invoked from within an asynchronous method,
        /// and only by the debugger.
        /// </remarks>
        internal void SetNotificationForWaitCompletion(bool enabled) =>
            SetNotificationForWaitCompletion(enabled, ref m_task);

        internal static void SetNotificationForWaitCompletion(bool enabled, [NotNull] ref Task<TResult>? taskField)
        {
            // Get the task (forcing initialization if not already initialized), and set debug notification
            (taskField ??= CreateWeaklyTypedStateMachineBox()).SetNotificationForWaitCompletion(enabled);

            // NOTE: It's important that the debugger use builder.SetNotificationForWaitCompletion
            // rather than builder.Task.SetNotificationForWaitCompletion.  Even though the latter will
            // lazily-initialize the task as well, it'll initialize it to a Task<T> (which is important
            // to minimize size for cases where an ATMB is used directly by user code to avoid the
            // allocation overhead of a TaskCompletionSource).  If that's done prior to the first await,
            // the GetMoveNextDelegate code, which needs an AsyncStateMachineBox, will end up creating
            // a new box and overwriting the previously created task.  That'll change the object identity
            // of the task being used for wait completion notification, and no notification will
            // ever arrive, breaking step-out behavior when stepping out before the first yielding await.
        }

        /// <summary>
        /// Gets an object that may be used to uniquely identify this builder to the debugger.
        /// </summary>
        /// <remarks>
        /// This property lazily instantiates the ID in a non-thread-safe manner.
        /// It must only be used by the debugger and tracing purposes, and only in a single-threaded manner
        /// when no other threads are in the middle of accessing this or other members that lazily initialize the task.
        /// </remarks>
        internal object ObjectIdForDebugger => m_task ??= CreateWeaklyTypedStateMachineBox();
    }
}
